package biolabs.model.abscretion

import biolabs.core.BioContext
import biolabs.core.BioObject
import biolabs.core.Genomic
import biolabs.model.CumulationStateful
import biolabs.model.division.DivisionDelegate
import biolabs.model.growth.GrowthDelegate
import biolabs.model.motion.MotionDelegate
import biolabs.space.ContinuousPosition
import biolabs.space.DiscretePosition
import biolabs.space.GridShapedObject

import scala.collection.JavaConversions._
import scala.collection.mutable.{Map => MMap, HashMap => MHashMap, Set => MSet}

object BorderAbscretor {
	trait Genome {
		val receptorGains: Map[Any, Iterable[(Any, Int)]]
		val secretions: Map[Any, Iterable[(Any, Int)]]
	}
}

/**
 * Object whose each border element holds
 * an {@link AbscretionInteractionPoint}.
 *
 * @author Tomas Mikula
 */
trait BorderAbscretor[
	Self <: BorderAbscretor[Self, P, C]
			with CumulationStateful[Self, P],
	P <: DiscretePosition[P, C],
	C <: ContinuousPosition[C, P]
] extends Abscretor[Self, P]
with BioObject
with GridShapedObject[P, C]
{

self: Self with Genomic[BorderAbscretor.Genome] =>

	private val ipoints = new MHashMap[Any, MMap[P, AbscretionPoint]];

	protected def init() {
		val substances = Set() ++ (
				genome.receptorGains.keys
				++ genome.secretions.keys
				++ genome.receptorGains.values.flatten.map { case (sub, gain) => sub }
				++ genome.secretions.values.flatten.map { case (sub, secr) => sub }
			)
		val border = getShape().getBorderElements()
		for(s <- substances) {
			val sMap = new MHashMap[P, AbscretionPoint]()
			for(b <- border.iterator) {
				sMap.put(b, new AbscretionPoint())
			}
			ipoints.put(s, sMap)
		}
		// checkReceptors()
	}

	override def proceed(startTime: Long, endTime: Long, ctx: BioContext) {
		for(step <- startTime until endTime) {

			// consume the absorbed substances
			for(m <- ipoints.values)
				for(ap <- m.values)
					ap.setAmount(0)

			increaseReceptors()
			secreteSubstances()
		}
	}

	override def abscretionDelegate(substance: Any): Option[AbscretionDelegate[P]] = {
		ipoints.get(substance) match {
			case Some(m) =>
				return Some(new AbscretionDelegate[P](substance, m.view))
			case None =>
				None
		}

	}

	override def getAbscretionPointsFor(substance: Any): Iterable[(P, AbscretionPoint)] = {
		ipoints.get(substance) match {
			case Some(m) => m.view
			case None => Seq()
		}
	}

	override def getAbscretionPointsAt(pos: P): Iterable[(Any, AbscretionPoint)] = {
		if(!getShape().getBorderElements().contains(pos))
			Seq()
		else
			for((s, m) <- ipoints.view)
				yield (s, m(pos))
	}

	override def getAbscretionPoint(substance: Any, pos: P): Option[AbscretionPoint] = {
		ipoints.get(substance) match {
			case Some(m) => m.get(pos) match {
				case Some(ap) => Some(ap)
				case None => None
			}
			case None => None
		}
	}

	def handleDivision(dd: DivisionDelegate[P, Self]) {
		initializeDaughter(dd.daughter())
		// daughter.checkReceptors()
		addMissingReceptors()
		// checkReceptors()
	}

	private def initializeDaughter(daughter: Self) {
		val border = daughter.getShape().getBorderElements()
		for((s, myMap) <- ipoints.view) {
			val thatMap = new MHashMap[P, AbscretionPoint]()
			for(b <- border.iterator) {
				myMap.remove(b) match {
					case Some(aip) =>
						thatMap.put(b, aip)
					case None =>
						thatMap.put(b, new AbscretionPoint())
				}

			}
			daughter.ipoints.put(s, thatMap)
		}
	}

	private def addMissingReceptors() {
		val rcptMap = ipoints.values.iterator.next
		for(b <- getShape().getBorderElements().iterator)
			if(!rcptMap.contains(b))
				for(m <- ipoints.values)
					m.put(b, new AbscretionPoint())
	}

	def handleAbscretion(ad: AbscretionDelegate[P]) {
		updateCumulationState()
	}

	/**
	 * Updates the cumulation state based
	 * on the amounts of substances on receptors.
	 */
	private def updateCumulationState() {
		var maxAmount = 0.0
		var maxSubstance = cumulationState
		for(x <- ipoints.entrySet()){
			var sum = 0.0
			for(y <- x.getValue().entrySet())
				sum += y.getValue().getSensedAmount();
			if(sum > maxAmount || (sum==maxAmount && x.getKey().equals(cumulationState))){
				maxAmount = sum;
				maxSubstance = x.getKey();
			}
		}
		cumulationState = maxSubstance;
	}

	/**
	 * Adds receptors to the membrane according to the
	 * current cumulation state and zygotic graph.
	 */
	private def increaseReceptors() {
		genome.receptorGains.get(cumulationState) match {
			case Some(gains) =>
				for((sub, gain) <- gains)
					addReceptors(sub, gain)
			case None =>
				// do nothing
		}
	}

	/**
	 * Adds {@code receptorCount} receptors to substance {@code substance}
	 * to <em>each</em> membrane element.
	 */
	private def increaseReceptors(substance: Any, receptorAmount: Int) {
		for(ap <- ipoints(substance).values)
			ap.increaseSensorCapacity(receptorAmount);
	}

	override def addReceptors(substance: Any, amount: Int) {
		val map = ipoints.get(substance) match {
			case Some(m) => m
			case None =>
				val m = new MHashMap[P, AbscretionPoint]()
				for(b <- getShape.getBorderElements)
					m.put(b, new AbscretionPoint())
				ipoints.put(substance, m)
				m
		}
		for(ap <- map.values)
			ap.increaseSensorCapacity(amount)
	}

	def handleMotion(md: MotionDelegate[P]) {
		for((sub, apoints) <- ipoints) {
			val newMap = apoints map { case (pos, ap) => (pos.plus(md.move), ap) }
			ipoints.put(sub, newMap)
		}
	}

	/**
	 * Secretes substances to the environment (interaction points)
	 * according to the current cumulation state and zygotic graph.
	 */
	private def secreteSubstances() {
		genome.secretions.get(cumulationState) match {
			case Some(secretions) =>
				for((sub, prod) <- secretions)
					for(ap <- ipoints(sub).values())
						ap.produce(prod)
			case None =>
				// do nothing
		}
	}

	override def handleGrowth(gd: GrowthDelegate[P]) {
		val shape = getShape().getElements()
		val newElems = MSet() ++ gd.elements
		val apMap = ipoints.values.head
		val inside = (p: P) => shape.contains(p) && !newElems.contains(p)
		val atBorder = (p: P) => p.getNeighbors().exists(n => !inside(n))
		for(p <- gd.elements) {
			newElems -= p
			// get elements that p caused to no longer be at the border
			val erase = p.getNeighbors().filter(
					// filter only elements that were at the border
					n => apMap.contains(n)
			).filter(
					// and all their neighbors ...
					n => n.getNeighbors().forall(
							// are inside the shape after adding p
							n2 => inside(n2)
					)
			)

			if(atBorder(p))			 // i.e. not closing a hole in the object
				for(m <- ipoints.values())				// for each substance
					m.put(p, new AbscretionPoint()) // create a receptor in p

			// for each element of erase redistribute its
			// receptors among its neighbor border elements
			for(q <- erase) {
				val targets = q.getNeighbors().filter(atBorder)
				val n = targets.size();
				for(m <- ipoints.values()){
					val ip = m.remove(q).get
					if(n!=0) {
						val a = ip.getAmount()
						val d = ip.getDemand()
						val aq = a / n
						val ar = a % n
						val dq = d / n
						val dr = d % n
						val saq = ip.getSensedAmount() / n
						val scq = ip.getSensorCapacity() / n
						var j = 0
						for(t <- targets) {
							val target = m(t)
							target.produce(aq+(if(j<ar) 1 else 0));
							target.increaseDemand(dq+(if(j<dr) 1 else 0));
							target.sensed += saq
							target.increaseSensorCapacity(scq)
							j += 1
						}
					}
				}
			}
		}
		// checkReceptors()
	}

	private def checkReceptors() {
		val border = getShape().getBorderElements()
		for(s <- ipoints) {
			val pos = s._2.keySet
			val missing = border--pos
			val excessive = pos--border
			var msg = ""
			if(!missing.isEmpty)
				msg += "Missing: " + missing + "\n"
			if(!excessive.isEmpty)
				msg += "Excessive: " + excessive + "\n"
			if(msg != "")
				throw new AssertionError("Object with shape " + getShape.getElements + " has invalid receptors:\n" + msg)
		}
	}
}
